import os, sys
import shutil
import copy
import re
import zipfile
import traceback
import time
from datetime import datetime

import docx
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.table import CT_Tbl
from docx.oxml.text.paragraph import CT_P
from docx.table import _Cell, Table
from docx.text.paragraph import Paragraph

try:
    import xml.etree.cElementTree as ET
except ImportError:
    import xml.etree.ElementTree as ET
import xlwt

import atexit
import platform
IS_WINDOWS = False
if platform.system()=="Windows":
    import win32process
    import win32api
    import win32com
    import win32com.client as win32 # 直接使用win32会报错
    import pythoncom
    from docx2pdf import convert
    IS_WINDOWS = True
import subprocess

import multiprocessing
import threading 
from pathlib import Path

from colorama import Fore, init
init(autoreset=True)

from hm.common import *
from hm.hos import *
from hm.xls import XLS
    

    
# +++++++++++++++++++ 备份文件夹 +++++++++++++++++++
MYDIR = os.path.dirname(__file__)
BACKUP_parent = os.path.join(MYDIR, 'backup')

def remove_old_back():
    old_dirs = os.listdir(BACKUP_parent)
    if len(old_dirs)>20:
        old_dirs = sorted(old_dirs, key=lambda x:os.path.getmtime(os.path.join(BACKUP_parent, x)))
        for i,fol in enumerate(old_dirs):
            if i==10:
                break
            try: # 日期转换可能失败
                shutil.rmtree(os.path.join(BACKUP_parent, fol))
            except:
                pass

class BACKUP:
    # 即使是单例模式，多进程下面还是会执行多次，不能保证只创建一个备份文件夹
    instance = None
    def __new__(cls):
        if cls.instance is None:
            cls.instance = super().__new__(cls)
            # # 所以弄了个临时文件f，保证程序只运行一次下面的代码
            cls.temp_file = os.path.join(BACKUP_parent, 'temp')
            current_process = multiprocessing.current_process()
            if current_process.name=='MainProcess': 
                remove_old_back()    
                with open(cls.temp_file, 'w') as f:
                    name = time.strftime("%Y%m%d%H%M%S", time.localtime())
                    cls.dir = os.path.join(MYDIR, 'backup', name)
                    os.makedirs(cls.dir, exist_ok=True)
                    f.write(cls.dir)
                    print(f"{Fore.LIGHTBLACK_EX}😶备份文件夹见: {cls.dir}")
            else:
                with open(cls.temp_file, 'r') as f:
                    cls.dir = f.read()
        return cls.instance

# 备份目录
BACKUP_DIR = BACKUP().dir


# ++++++++++++++++++++ 日志对象 +++++++++++++++++++++++
def init_logger():
    path = os.path.join(MYDIR, "odbc.log")
    if multiprocessing.current_process().name=='MainProcess': 
        with open(path, 'w') as f: # 清空文件内容
            f.write('start log')
    return HmLog(path)

log = init_logger()
logger = log.logger


# ++++++++++++++++++++ win32com word对象 +++++++++++++++++++++++
# def fix_gen_py():
#     # 解决gen_py报错问题
#     # 方法一
#     user_path = os.path.expanduser('~')
#     genpy = os.path.join(user_path, 'AppData', 'Local', 'Temp', 'gen_py')
#     v = sys.version_info
#     py_path = os.path.join(genpy, f"{v[0]}.{v[1]}")
#     if os.path.exists(py_path):
#         for n in os.listdir(py_path):
#             path = os.path.join(py_path, n)
#             if os.path.isdir(path):
#                 if re.search(r'(\w{4}-){3}', n) is not None:
#                     shutil.rmtree(path)
#                     print(f"{Fore.GREEN} 移除gen_py{path}成功")
#     #方法二
#     # excel = win32.gencache.EnsureDispatch('Excel.Application') # 或Word
#     # #change to =>
#     # excel = win32.Dispatch('Excel.Application')
    
#     # 方法三
#     # print("处理genpy", win32.gencache.GetGeneratePath())  # 这将打印出缓存的路径)
#     # shutil.rmtree(os.path.join(os.environ.get('LOCALAPPDATA'), 'Temp', 'gen_py'))
    
    
# def close_word_by_force(word_app):
#     # Get the window's process id's
#     hwnd = word_app.Hwnd
#     t, p = win32process.GetWindowThreadProcessId(hwnd)
#     try:
#         handle = win32api.OpenProcess(win32com.PROCESS_TERMINATE, 0, p)
#         if handle:
#             win32api.TerminateProcess(handle, 0)
#             win32api.CloseHandle(handle)
#             print("关闭word成功")
#     except:
#         pass
#     # subprocess.run(["taskkill", "/F", "/IM", "WINWORD.EXE"])

@atexit.register
def quit_app():
    current_process = multiprocessing.current_process()
    if current_process.name=='MainProcess': # 防止多进程调用多次
        WordApp.Quit()


class WordApp:
    instance = None
    def __new__(cls):
        if cls.instance is None:
            for attempt in range(2):
                try:
                    # pythoncom.CoInitialize() # 多线程不加这个会报错
                    cls.app = win32.gencache.EnsureDispatch('Word.Application')
                    # cls.app = win32.Dispatch('Word.Application')
                    # cls.app = win32.dynamic.Dispatch('Word.Application')
                    # cls.app = win32.DispatchEx('Word.Application') # 使用独立进程打开
                    cls.app.Visible = False
                    cls.app.DisplayAlerts = 0
                    cls.instance = super().__new__(cls)
                except AttributeError as e:
                    if e.name == 'CLSIDToClassMap':
                        print("tring to reopen word...wait", end='')
                        mod_name = e.obj.__name__
                        mod_name_parts = mod_name.split('.')
                        if len(mod_name_parts) == 3:
                            # Deleting the problematic module cache folder
                            folder_name = mod_name_parts[2]
                            gen_path = Path(win32com.__gen_path__)
                            folder_path = gen_path.joinpath(folder_name)
                            shutil.rmtree(folder_path)
                            # Deleting the reference to the module to force a rebuilding by gencache
                            del sys.modules[mod_name]
                            continue
                        raise Exception("There was an error loading Excel.") from e
        return cls.instance

    @classmethod
    def Quit(cls):
        if cls.instance:
            cls.app.Quit()
            del cls.app
            # pythoncom.CoUninitialize()
            print("Quit word application")


class WordDocument:
    def __init__(self, docpath=''):
        self.docpath = docpath
        self.app = WordApp().app
    
    def __enter__(self):
        if self.docpath:
            self.doc = self.app.Documents.Open(self.docpath)
        else:
            self.doc = self.app.Documents.Add()
        return self.doc
        
    def __exit__(self, *args): 
        self.doc.Close()
        

def merge_docx_abandoned(folder):
    '''
        python-docx合并多个docx内容（已弃用）
    '''
    paths = []
    for i in os.listdir(folder):
        if i.endswith(".docx") and not OSis_hidden(i):
            paths.append(OS.join(folder, i))
    # 创建一个新的Word文档
    print("start merging docx file")
    new_doc = Document()
    for path in paths:
        # 加载每个文档
        doc = Document(path)
        # 将每个文档的内容添加到新文档中
        for element in doc.element.body:
            new_doc.element.body.append(element)
    # 保存合并后的文档
    save_path = OSrep_name(paths[0], 'merge_output')
    new_doc.save(save_path)
    print(f"merged file has been saved in:{save_path}")
    
    
def merge_docx(folder):
    '''
        win32com合并多个docx文件内容为一个docx
    '''
    if not IS_WINDOWS:
        return merge_docx_abandoned(folder)
    # 获取文件夹下面的docx
    paths = []
    for i in os.listdir(folder):
        if i.endswith(".docx") and not OSis_hidden(i):
            paths.append(OS.join(folder, i))
    # 创建副本，防止对原文件造成任何改动
    copy_paths = []
    for i, docx_path in enumerate(paths):
        print(f"{i}、copy and auto_numbet_to_text {docx_path}")
        copy_path = OSrep_name(docx_path, OSname(docx_path)+"-copy")
        shutil.copy2(docx_path, copy_path)
        auto_number_to_text(copy_path)
        copy_paths.append(copy_path)
    save_path = OSrep_name(paths[0], 'merge_output')
    if save_path in copy_paths:
        copy_paths.remove(save_path)
    
    print("start merging docx file")
    # 更新20250317
    with WordDocument() as output_docx:
        output_content = output_docx.Content
        for i, docx_path in enumerate(copy_paths): 
            output_content.Collapse(0) # 相当于文件指针seek到末尾
            output_content.InsertFile(docx_path)
            output_content = output_docx.Content # 没有这一行，插入顺序将是反的
            print(f"{i}、merge {docx_path}")
        
        # 清理副本
        for i, docx_path in enumerate(copy_paths): 
            print(f"{i}、remove {docx_path}")
            os.remove(docx_path)

        # 保存输出
        output_docx.SaveAs(save_path)
        print(f"merged file has been saved in:{save_path}")
        output_docx.Close()


def to_pdf(docx_path):
    '''
        转换docx对象为pdf
    '''
    pdf_path = os.path.join(os.path.dirname(docx_path), OSname(docx_path)+".pdf")
    if os.path.exists(pdf_path):
        yes_no = input(f'文件{pdf_path}已存在，是否覆盖？Y/n')
        if yes_no not in ["Y",'y']:
            return pdf_path
    f = open(pdf_path, 'w')
    f.close()
    convert(docx_path, pdf_path)
    print(f'{docx_path}转换为pdf成功')
    time.sleep(1)
    return pdf_path


# 获取word文档总页数
def get_page_count_by_xml(docx_path):
    '''
        从文档元数据 获取页码，仅支持新版(office2019以上？)docx文档
    '''
    docx_object = zipfile.ZipFile(docx_path)
    docx_property_file_data = docx_object.read('docProps/app.xml').decode()
    page_count = re.search(r"<Pages>(\d+)</Pages>", docx_property_file_data)
    if page_count:
        page_count = int(page_count.group(1))
    # print("warnning: get page from the word medata maybe not correct!")
    return page_count


def get_page_count_by_win32(docx_path):
    '''
        以word应用对象获取页码
    '''
    # 新版本20250317
    if not IS_WINDOWS:
        return get_page_count_by_xml(docx_path)
    with WordDocument(docx_path) as doc:
        doc.Repaginate() 
        pages = doc.ComputeStatistics(2)
        # doc.Close(SaveChanges=0)
        return int(pages) # __exit__方法同样会被调用
    # 方法一 这种方法只有在Visible=True时能正确读取，而且经常失败
    # pages = doc.BuiltInDocumentProperties(14).Value 
    # properties = doc.BuiltInDocumentProperties # 效果同上面一行
    # pages = properties('Number of Pages').Value

    # 方法二 可能是先分页，后对页数进行了统计，但不清楚原理
    # WORD_APP.ActiveDocument.Repaginate()
    # pages = WORD_APP.ActiveDocument.ComputeStatistics(2)
  

def detect_auto_number(path):
    try:
        doc = Document(path)
        doc_xml = doc.element.xml
        root = ET.fromstring(doc_xml)
        # print(doc_xml.count("w:numPr"))
        # <w:ilvl w:val="0"/>
        # <w:numId w:val="1"/>
        w = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
        if "w:numPr" in doc_xml:
            for node in root.iter(w+"numPr"):
                for child in node.iter(w+"numId"):
                    if child.attrib[w+'val'] != '0':
                        # print("文档含有自动编号")
                        return True 
    except Exception as e:
        logger.info(f"文档无法打开，无法读取自动编号:{path}, {str(e)}\n")
    return False
    
    

def auto_number_to_text(path):
    '''
    自动编号转常规文本
    '''
    if IS_WINDOWS:
        # 新版本20250317
        if detect_auto_number(path):
            # print(f"{Fore.LIGHTBLACK_EX}文档{os.path.basename(path)}含自动编号，正在识别...", end=" ")
            try:
                with WordDocument(path) as doc:
                    doc.ConvertNumbersToText() # todo 多进程模式下，此行代码会报错，呼叫word失败
                    doc.Save()
                    # print("ok 注意，文件会被保存为非自动编号\n")
            except Exception as e:
                # print("识别失败\n", str(e))
                logger.info(f"{path}自动编号读取失败")
                
        # for parg in doc.Paragraphs:
        #     parg.Range.ListFormat.ConvertNumbersToText()
        # for tb in doc.Tables:
        #     for i in range(1, tb.Rows.Count+1):
        #         for j in range(1, tb.Columns.Count+1):
        #             cell = tb.Cell(1, 1)
        #             if cell.Range.ListFormat.ListString:
        #                 cell.Range.ListFormat.ConvertNumbersToText()
        #                 print("**********************", cell.Range.ListFormat.ListString)
        #     # tb.Range.ListFormat.ConvertNumbersToText()
        #     # print("**********************", tb.Range.ListFormat.ListString)
        

def auto_number_to_text_many(paths):
    # import time
    # start = time.time()
    # ts = []
    print(f"{Fore.LIGHTBLACK_EX}😶正在读取含自动编号的文件", end='')
    for path in paths:
        print(f"{Fore.LIGHTBLACK_EX}.", end='')
        filename = os.path.basename(path)
        copy_path = os.path.join(BACKUP_DIR, filename)
        if not os.path.exists(copy_path): 
            shutil.copy2(path, copy_path)
        auto_number_to_text(path)
    print(f"{Fore.LIGHTBLACK_EX}注意：文件会保存为非自动编号")
    #     t = threading.Thread(target=auto_number_to_text, args=(path, ))
    #     t.setDaemon(True)
    #     t.start()
    #     ts.append(t)
    # for t in ts:
    #     t.join()
    # print("耗时", time.time()-start)
    
       

class HmDocx:
    def __init__(self, path):
        self.path = path
        self.filename = os.path.basename(path)
        try:
            self.copy_path = os.path.join(BACKUP_DIR, self.filename)
            if not os.path.exists(self.copy_path): 
                # user could use HmDocx for same file many times
                # so we only backup at first time, because the second time the file probably changed.
                shutil.copy2(self.path, self.copy_path)
       
            # auto_number_to_text(path)
            self.doc = Document(path)
            self.tables = self.doc.tables
            self._doc_string = ''
            self._empty_cell = []
            # 检查文件是否开启修订模式
            self.is_revised = False
            self.detect_revised()
            # self.original_doc_xml = copy.deepcopy(self.doc.element.xml)
            # self.PAGEs = get_page_count_by_xml(self.path)
            self.PAGEs = 0
        except Exception as e:
            logger.warning("不是docx文件"+traceback.format_exc())
            raise AssertionError("不是docx文件", self.path)
    
    def detect_revised(self):
        '''
            检查是否有修订内容，有的话，接受所有修订
        '''
        # 这一步比较耗时，但是为了检测中文和正确读取修订内容又不得不这么做
        for p in self.doc.paragraphs:
            self._doc_string += self.accept_all(p)
        for i,t in enumerate(self.tables):
            for j,row in enumerate(t.rows):
                try:
                    for k,cell in enumerate(row.cells):
                        for p in cell.paragraphs:
                            self._doc_string += self.accept_all(p)
                        # if not cell.text:
                        #     # 检测空的单元格，其实与这个函数无关
                        #     self._empty_cell.append([i, j, k])
                        # else:
                        #     if "electronic signature" in cell.text:
                        #         # 修改签名，其实与这个函数无关
                        #         self.replace_para_content("electronic signature", "wet signature", cell, font_rgb=RGBColor(0x00, 0x00, 0x00))
                except:
                    pass

    def accept_all(self, p):
        """
            Return text of a paragraph after accepting all changes
            '''
                # 插入的内容
                <w:ins w:id="6" w:author="海梦 蓝" w:date="2024-03-23T09:55:00Z">
                    <w:r>
                    <w:rPr>
                        <w:rFonts w:hint="eastAsia"/>
                    </w:rPr>
                    <w:t>我是修订内容</w:t>
                    </w:r>
                </w:ins>
                # 删除的内容
                <w:del w:id="5" w:author="海梦 蓝" w:date="2024-03-23T10:30:00Z">
                    <w:r w:rsidDel="00646464">
                    <w:delText>Parameters Test</w:delText>
                    </w:r>
                </w:del>
            '''
        """
        xml = p._p.xml
        if "w:del" in xml or "w:ins" in xml:
            self.is_revised = True
            tree = ET.fromstring(p._p.xml)
            # xml命名空间
            word_body = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
            text_tag  = word_body + "t" # 修订的内容
            ins_tag = word_body+"ins"
            del_tag   = word_body + "delText"
            p.text = ''.join([node.text for node in tree.iter(text_tag) if node.text])
        text = p.text
        if "\xa0" in text:
            p.text = text.replace("\xa0", " ")
        return p.text


    def reject_all(self, p):
        """
            Return text of a paragraph after rejecting all changes
        """
        word_body = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
        text_tag  = word_body + "t"
        del_tag   = word_body + "delText"
        ins_tag   = word_body + "ins"
        xml = p._p.xml
        if "w:del" in xml or "w:ins" in xml:
            tree = XML(xml)
            # find and remove all insert tags
            for insert in tree.findall(ins_tag):
                tree.remove(insert)
            # find all deletion tags and change to text tags
            for deletion in tree.getiterator(del_tag):
                deletion.tag = text_tag
            runs = (node.text for node in tree.getiterator(text_tag) if node.text)
            return "".join(runs)
        else:
            return p.text
    
    def iter_block_items(self, parent):
        """
        按顺序遍历doc文档的段落和表格，返回段落或者表格对象
        Generate a reference to each paragraph and table child within *parent*,
        in document order. Each returned value is an instance of either Table or
        Paragraph. *parent* would most commonly be a reference to a main
        Document object, but also works for a _Cell object, which itself can
        contain paragraphs and tables.
        用法见下面的cp_tables
        """
        import docx
        if isinstance(parent, docx.document.Document):
            parent_elm = parent.element.body
            # print(parent_elm.xml)
        elif isinstance(parent, _Cell):
            parent_elm = parent._tc
        else:
            raise ValueError("something's not right")

        for child in parent_elm.iterchildren():
            if isinstance(child, CT_P):
                yield Paragraph(child, parent)
            elif isinstance(child, CT_Tbl):
                yield Table(child, parent)
    
    def cp_table(self, table):
        '''
            复制一个table对象，并粘贴它下面
        '''
        # 获取该表格是文档中第几个表格
        target_tb_ix = self.tables.index(table) 
        # 获取该表格在文档中哪个段落（因为要利用 段落对象插入table的方法）
        p_ix, tb_ix = 0, 0
        for i,x in enumerate(self.iter_block_items(self.doc)):
            if isinstance(x, Paragraph):
                p_ix += 1
            if isinstance(x, Table):
                if tb_ix==target_tb_ix:
                    break
                tb_ix += 1
        # 在该表格后面的段落插入新表格
        # todo：如果该表格位于连续的几个表格之间，则新表格插入位置可能不对
        # todo：p_ix+1可能超出self.doc.paragraphs索引
        if p_ix:
            p = self.doc.paragraphs[p_ix+1] # 再往下一段
            p.insert_paragraph_before() # 插入空行
            p = self.doc.paragraphs[p_ix+1] # 在插入的空行处插入表格
            t = copy.deepcopy(table._tbl) # 复制表格内容
            p._p.addnext(t) # 插入表格
            t = Table(t, self.doc) # 创建表格容器
            self.tables.insert(tb_ix+1, t) 
            return t

    def cp_table_to_para(self, table, p):
        '''
            copy a table to the behind of paragraph
        '''
        new_table = copy.deepcopy(table)
        p._p.addnext(new_table._element)
        # tofo self.tables.insert(tb_ix+1, t) 
        return new_table


    def cp_run_style(self, source, new, **kwargs):
        '''
            source和new都是run对象
            复制source的格式给new
        '''
        new.style = source.style
        new.font.name = source.font.name
        new.font.bold = source.font.bold
        new.font.italic = source.font.italic
        new.font.size = source.font.size
        new.font.color.rgb = source.font.color.rgb

    def cp_para_style(self, source, new, **kwargs):
        '''
        复制段落source（的第一个run）格式给new段落
        '''
        # 修改新段落style
        new.style = source.style
        alignment = {"left":WD_ALIGN_PARAGRAPH.LEFT,
            "right":WD_ALIGN_PARAGRAPH.RIGHT
        }
        # 修改段落对齐
        if "alignment" in kwargs:
            # 用户自定义样式
            new.paragraph_format.alignment = alignment[kwargs["alignment"]]
        else:
            new.paragraph_format.alignment = source.paragraph_format.alignment
        
        if not new.runs: # 段落不一定有run(单元格似乎一定有段落)    
            print("+++++++++++++++++++++add new run")
            new.add_run() # 新增空的run（下次插入内容会自动应用它的格式）
            
        for new_run in new.runs:
            # 默认样式
            new_run.font.name = "Times New Roman"
            new_run.font.bold = False
            new_run.font.italic = False
            new_run.font.size = Pt(10.5) # 五号
            new_run.font.color.rgb = RGBColor(0x00,0x00,0xFF) # 默认蓝色字体
            # 要复制的样式
            if source.runs:
                self.cp_run_style(source.runs[0], new_run, **kwargs)
            # 用户自定义样式
            if "font_rgb" in kwargs:
                new_run.font.color.rgb = kwargs.pop("font_rgb")
            for key in kwargs: # todo指明name, bold, italic, size
                setattr(new_run.font, key, kwargs[key])
        
    def clear(self, cell):
        '''
            清除单元格内容
        '''
        for p in cell.paragraphs:
            p.clear()
    
    def insert_paragraph(self, index, content, cell, clear=False, **kwargs):
        '''
            功能：在[单元格cell or doc]对象中插入新内容，并保持原有段落的格式
            index: 此段落（插入单元格之后）相对其他段落的索引值：
                   假设单元格原本有9个段落，index=9，那么在原本的第9个段落前插入新段落，原本的第9个段落变成第10个
                   index还支持负索引，假设原本有9个段落，index=-1表示在第9个段落之后插入，新段落变成第10个段落
            content: 插入新段落内容
            cell: 可以是docx表格的单元格对象，也可以是其他支持添加段落的对象
            clear: 插入前是否清空[单元格]
        '''
        len_p = len(cell.paragraphs)
        # 1. 处理索引
        # 处理后的index范围是 [0, len_p]
        if index<0: # 转换负索引为正索引
            index = len_p+index+1 
        if index<0:
            index=0 # 最小在第1个插入
        if index>len_p:
            index = len_p # 最大在最后一个插入
        # 2. 拷贝目标位置原来的样式
        source_p = None
        if len_p:
            # 优先使用插入位置前面一个段落的样式
            # (如果index=0，则index-1为最后一个段落的样式)
            source_p = copy.deepcopy(cell.paragraphs[index-1]) # 先拷贝，防止下面if clear被清空
        # 3. 是否先清空原本内容
        if clear:
            # todo 清除之后source_p就没了
            self.clear(cell)
        # 4. 插入新段落内容
        if clear or not len_p: # 直接赋值
            cell.text = content
            p = cell.paragraphs[0]
        elif index<len_p: # 在原来某个段落前面插入
            cell.paragraphs[index].insert_paragraph_before(content)
            p = cell.paragraphs[index]
        else: # 添加到最后一段
            cell.add_paragraph(content)
            p = cell.paragraphs[-1]
        # 5. 修改新段落样式
        if source_p:
            kwargs["font_rgb"] = RGBColor(0x00,0x00,0xFF) # 蓝色
            self.cp_para_style(source_p, p, **kwargs)
    
    def replace_para_content(self, old, new, cell, **kwargs):
        '''
            替换单元格中某一段的部分或全部内容，并标注为蓝色
            old：旧内容
            new：新内容
        '''
        # 要替换的内容在某个段落内
        for i, p in enumerate(cell.paragraphs):
            source_p = copy.deepcopy(p)
            matchs = [i for i in re.finditer(re.escape(old), source_p.text)]
            if matchs:
                p.clear()
                new_runs = []
                start = 0
                for match in matchs:  
                    p.add_run(source_p.text[start:match.start()])
                    run = p.add_run(new, style=source_p.runs[0].style)
                    new_runs.append(run)
                    start = match.end()
                if start<len(source_p.text):
                    p.add_run(source_p.text[start:])
                self.cp_para_style(source_p, p)
                for run in new_runs:
                    if "font_rgb" not in kwargs:
                        run.font.color.rgb = RGBColor(0x00, 0x00, 0xFF)  
                    else:
                        run.font.color.rgb = kwargs["font_rgb"]

    def delete_para_content(self, content, cell):
        '''
            删除单元格中某一段的部分内容
        '''
        self.replace_para_content(content, '', cell) 

    def save(self):
        self.doc.save(self.path)
            

class HmTable(HmDocx):
    ATTR = {}
    def __init__(self, t, parent=None):
        self.table = t
        self.rows = t.rows
        self.parent = parent # 用这个类（HmTable）创建实例的类
        self.init_ATTR()

    def init_ATTR(self):
        '''
            通过ATTR，将表格对象转化为 类属性，然后重写__setattr__方法实现通过操作属性来改变表格内容
            可参考index_DTR.py的IndexHeaderTbl用法：
            ATTR 的键是类属性名，值是对应表格的行列位置，以及该单元格原本的的内容（通过regex匹配）
                ATTR = {
                    "platform":{"row":1, "col":1, 'regex':r' *Platform: *(.+?)\nGUI'},
                    "GUI":{"row":1, "col":1, 'regex':r'GUI: *(.+?)(MACO)|.$'},
                    "date":{"row":0, "col":4},
                    "compiler":{"row":1, "col":4}
                }
            1. 如果row和col都存在，regex不存在，则直接匹配row和col对应的单元格，否则按regex匹配单元格
            2. 如果'row_offset'或'col_offset'存在，则在1匹配结果基础上偏移行和列
            3. 有正则，无子组
                    读取值如果有正则匹配到的内容，则读取移除匹配内容后的值
                    写入值，正则匹配的内容保留
            4. 有正则子组, 
                1）读取，假设platform单元格原本内容为：
                VITROS LVCC 450 Chemistry System Environmental Test Plan and Procedure
                Platform:
                        45001020
                        45001025
                GUI:  
                        v1.0.0.125
                        v1.0.0.126
                        V1.0.0.128
            按照正则 只匹配到 45001020 和 45001025（子组），则只读取子组
                2）写入，如果操作self.platform = '45001038', 那么上面的单元格内容， 
            45001020 和 45001025 将被替换为'45001038' （只修改子组匹配的内容）
            5. 正则如果匹配不到内容，读取值为None，并无法写入值
            6. 子组和offset不能同时设定，因为1）偏移是在正则找到的单元格(a)之后转移到单元格(b)，2）而子组要操作的是正则找到的单元格(a)
                1）和2）矛盾，程序无法判断到底要操作单元格a还是b
        '''
        for key in self.ATTR:
            value =  None 
            v = [None, None, 0, 0] # 用None是因为row为0也是有效的行
            n = ['row', 'col', 'row_offset', 'col_offset']
            for i,s in enumerate(n):
                if s in self.ATTR[key]:
                    v[i] = self.ATTR[key][s]
            row, col, row_offset, col_offset = v
            if isinstance(row, int) and isinstance(col, int):
                value = self.rows[row+row_offset].cells[col+col_offset].text.strip()
            if 'regex' in self.ATTR[key]: # row和col此时仍可能是None
                x = self.locate_cell(self.ATTR[key]['regex'],
                                     flags= re.I|re.DOTALL, row=row, col=col)
                if x:
                    row = x[0] # 如果正则能匹配，row和col肯定是数字
                    col = x[1]
                    match = x[3]
                    if match.groups():
                        value = x[2]
                    else:
                        value = self.rows[row+row_offset].cells[col+col_offset].text.strip()
                        # x[2]为完全匹配 或子组 的值
                        # 如果有正则，没有子组，value包含匹配的内容，要移除掉
                        # 比如，原本的value为Tester: Haimeng，正则为Tester:，则新value为Haimeng
                        value = value.replace(x[2], '').strip()
                else:
                    value = None
            for i,s in enumerate(n):
               self.ATTR[key][s] = [row, col, row_offset, col_offset][i] # __setattr__ 需要
            setattr(self, key, value)
    
    def __setattr__(self, key, value):
        '''
            与init_ATTR方法搭配，通过操作属性来操作单元格
            key: 这个类的属性
            value: 要设置的单元格的内容
        '''
        if key in self.ATTR: 
            if hasattr(self, key) and getattr(self, key)!=value: # 有此属性且内容不一样再修改(避免与init_ATTR死循环)
                row, col, row_offset, col_offset = self.ATTR[key]['row'], self.ATTR[key]['col'], self.ATTR[key]['row_offset'], self.ATTR[key]['col_offset']
                if isinstance(row, int) and isinstance(col, int): # 上面init_ATTR初始化过key的row和col
                    cell = self.rows[row].cells[col]
                    cell_offset = self.rows[row+row_offset].cells[col+col_offset]
                    content = value
                    # 如果有子组，只改变子组匹配到的内容，
                    # 如platform:45001025, 只想改变仪器编号
                    if "regex" in self.ATTR[key]:
                        ret = re.search(self.ATTR[key]['regex'], cell.text, flags=re.I|re.DOTALL)
                        if ret:
                            if ret.groups():
                                content = cell.text[:ret.start(1)] + value + cell.text[ret.end(1):] # value仅替换第一个子组匹配的值
                            else: # 没有子组，且目标单元格包含匹配的内容，匹配内容要保留
                                if ret.group() in cell_offset.text:
                                    content = ret.group() + value 
                        else: # 匹配不到，不操作单元格
                            return
                    self.set_content(content, cell)
        super().__setattr__(key, value)
    
    def locate_cell(self, regex, flags=None, row=None, col=None):
        '''
        根据传入的正则表达式，查找匹配的单元格位置和内容
        '''
        rows = [self.rows[row]] if row is not None else self.rows
        for i,r in enumerate(rows):
            cells = [r.cells[col]] if col is not None else r.cells
            for j,c in enumerate(cells):
                if not flags:
                    flags = re.I # 默认不区分大小写
                match = re.compile(regex, flags=flags).search(c.text)
                if match:
                    # 默认如果存在子组，则返回第一个匹配的子组
                    if not match.groups():
                        value = match.group()
                    else:
                        value = match.group(1) 
                    if not value:
                        value = '' # 因为None没有strip方法
                    value = value.strip().strip("\n")
                    i_offset = row if row is not None else 0
                    j_offset = col if col is not None else 0
                    return [i+i_offset, j+j_offset, value, match]

    def is_merge(self, cell1, cell2, direction='x'):
        '''
            判断两个单元格cell1和cell2是否为合并的单元格
        '''
        if direction=='x' and cell1._tc.right == cell2._tc.right:
            return True
        if direction=='y' and cell1._tc.bottom == cell2._tc.bottom:
            return True
        return False

    
    def cp_row_fmt(self, source, new, **kwargs):
        '''
            复制source行格式到new行
        '''
        source_cells = source.cells
        source_cells_len = len(source_cells)
        for i,s_c in enumerate(source_cells):
            n_c = new.cells[i]
            # 1. 依照source合并单元格
            # print(s_c.text, s_c._tc.top, s_c._tc.bottom, s_c._tc.left, s_c._tc.right)
            if i+1 < source_cells_len -1:
                # if s_c._tc.right==source.cells[i+1]._tc.right: 
                if self.is_merge(s_c, source_cells[i+1]): # 需要合并(当前单元格和下一个单元格左右列数一样)
                    n_c.merge(new.cells[i+1])
            # 2. 依照source设置段落
            source_p = s_c.paragraphs[0]
            new_p = n_c.paragraphs[0]
            # 因为如果new_P没有run对象，字体样式无法拷贝
            if not new_p.runs:
                new_p.add_run()
            self.cp_para_style(source_p, new_p, **kwargs)

    def insert_row(self, ix, cp_pre_fmt=True):
        '''
            在ix插入新行
            cp_pre_fmt: 复制前一行（如果有）的格式
        '''
        if ix<0:
            ix = len(self.rows)+ix+1 # ix=-1表示添加到最后一行
        if ix<len(self.rows): # 中间插入行
            tbl = self.table._tbl
            successor = tbl.tr_lst[ix]
            tr = tbl._new_tr()
            for gridCol in tbl.tblGrid.gridCol_lst:
                tc = tr.add_tc()
                tc.width = gridCol.w
            successor.addprevious(tr)
            row = self.rows[ix]
        else:
            row = self.table.add_row() # 末尾添加行
        if cp_pre_fmt:
            source = self.rows[ix-1] if ix>0 else self.rows[0]
            self.cp_row_fmt(source, row)
        return row

    def delete_row(self, row):
        '''
            删除行
        '''
        if isinstance(row, int):
            row = self.rows[row]
        row._element.getparent().remove(row._element) 

    def set_content(self, content, cell):
        '''
            设置单元格内容
        '''
        self.insert_paragraph(0, content, cell, clear=True)
        


# if __name__=="__main__":
#     path = r"E:\工作\VTSU\QST-ODBC-M2-VTSU-R0004 LVCC VersaTip Supply（VTSU）Subsystem Test Report.docx"
#     doc = HmDocx(path)
#     print("页码", doc.PAGEs)